//
//  TwitterClient.m
//  trowel
//
//  Created by Jonathan Reams on 1/8/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import <SystemConfiguration/SCNetworkReachability.h>
#import "TwitterClient.h"
#import "JSON.h"
#include <Security/Security.h>
#import <IOKit/IOKitLib.h>
#import "NewTweetDialog.h"

@implementation TwitterClient
@synthesize statusMenu;

- (id) init {
	if(self = [super init]);
	{
		[GrowlApplicationBridge setGrowlDelegate:self];
		profileImageCache = [NSMutableDictionary dictionaryWithCapacity:40];
		inCheck = NO;
	}
	return self;
}

- (NSNumber*) getIdleTime {
	mach_port_t masterPort;
	io_iterator_t iter;
	io_registry_entry_t curObj;
	
	IOMasterPort(MACH_PORT_NULL, &masterPort);
	IOServiceGetMatchingServices(masterPort, IOServiceMatching("IOHIDSystem"), &iter);
	if(iter == 0) 
		return [NSNumber numberWithInt:0];

	curObj = IOIteratorNext(iter);
	if(curObj == 0) 
		return [NSNumber numberWithInt:0];
	
	CFTypeRef obj = IORegistryEntryCreateCFProperty(curObj, CFSTR("HIDIdleTime"), kCFAllocatorDefault, 0);
	uint64_t tHandle = 0;
	if(obj) {
		
		CFTypeID type = CFGetTypeID(obj);
		if(type == CFDataGetTypeID()) {
			CFDataGetBytes((CFDataRef) obj, CFRangeMake(0, sizeof(tHandle)), (UInt8*)&tHandle);
		} else if (type == CFNumberGetTypeID()) {
			CFNumberGetValue((CFNumberRef)obj, kCFNumberSInt64Type, &tHandle);
		} else{
			return [NSNumber numberWithInt:0];
		}
		tHandle >>= 30;
	}

	if(curObj != 0) IOObjectRelease(curObj);
	if(iter != 0) IOObjectRelease(iter);
	return [NSNumber numberWithLongLong:tHandle];
}

- (NSDictionary *) postTweetsForData:(NSString *)jsonData forUsername: (NSString*) username except: (NSHashTable*) alreadySeen {
	SBJsonParser * jsonParser = [[SBJsonParser alloc] init];
	
	id returnedObject = [jsonParser objectWithString:jsonData];
	if([returnedObject isKindOfClass:[NSDictionary class]])
	{
		NSString * errorMessage = @"Twitter returned garbage!";
		NSMutableDictionary * dataDict = returnedObject;
		if([dataDict objectForKey:@"error"] != nil)
			errorMessage = [dataDict objectForKey:@"error"];
		[GrowlApplicationBridge notifyWithTitle:@"Twitter Error!"
									description:errorMessage 
							   notificationName:@"Error!" 
									   iconData:nil 
									   priority:2 
									   isSticky:[[config getGlobalSetting:@"stickyTweets"] boolValue] 
								   clickContext:nil];
		return [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 1], @"maxid", [NSString string], @"lasttweet", nil];
	}
	
	NSMutableArray * tweetArray = (NSMutableArray*)returnedObject;
	NSNumber * maxID = [NSNumber numberWithInt:0];
	NSString * lastTweet = [NSString string];
	NSInteger i = 0;
	for(i = 0; i < [tweetArray count] && i < [[config getGlobalSetting:@"countPerUpdate"] intValue]; i++)
	{
		NSMutableDictionary * curTweet = [tweetArray objectAtIndex:i];
		NSNumber * curID = [curTweet objectForKey:@"id"];
		NSString * tweetText = [curTweet objectForKey:@"text"];
		if([alreadySeen containsObject:curID])
			continue;
		else 
			[alreadySeen addObject:curID];
		if([curID longLongValue] > [maxID longLongValue])
		{
			maxID = curID;
			lastTweet = [NSString stringWithString: tweetText];
		}
		NSMutableDictionary * userData = [curTweet valueForKey:@"user"];
		if(userData == nil)
			userData = [curTweet objectForKey:@"sender"];
		NSString * fullName = [userData valueForKey:@"name"];
		NSString * screenName = [userData valueForKey:@"screen_name"];
		NSString * profileImgUrl = [userData valueForKey:@"profile_image_url"];
	
		NSData * profileImgData = nil;
		NSImage * profileImg = nil;
		if([[config getGlobalSetting:@"showProfilePics"] boolValue])
		{
			NSDictionary * cachedPic = [profileImageCache objectForKey:screenName];
			if(cachedPic != nil)
			{
				NSString * cachedPicUrl = [cachedPic objectForKey:@"url"];
				if([cachedPicUrl compare:profileImgUrl] == 0)
					profileImgData = [cachedPic objectForKey:@"data"];
			}
			if(profileImgData == nil)
			{
				profileImgData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:profileImgUrl]];
				NSDictionary * cachedPic = [NSDictionary dictionaryWithObjectsAndKeys:profileImgData, @"data", profileImgUrl, @"url", nil];
				[profileImageCache removeObjectForKey:screenName];
				[profileImageCache setObject:cachedPic forKey:screenName];
			}
			profileImg = [[NSImage alloc] initWithData:profileImgData];
		}
		NSString * title = [NSString stringWithFormat:@"%@ (%@)", fullName, screenName];
		NSMutableDictionary * reTweetData = [curTweet objectForKey:@"retweeted_status"];
		NSString * notificationName = nil;
		if([curTweet objectForKey:@"in_reply_to_screen_name"] != [NSNull null] && 
		   [((NSString*)[curTweet objectForKey:@"in_reply_to_screen_name"]) compare:username] == 0)
			notificationName = @"Reply!";
		else if([reTweetData count] > 0)
			notificationName = @"Retweet!";
		else if([tweetText length] == 140)
			notificationName = @"Twoosh!";
		else
			notificationName = @"Tweet!";
		
		BOOL stickyTweets = NO;
		NSRange mentionLock = [tweetText rangeOfString:[NSString stringWithFormat:@"@%@", username] options: NSCaseInsensitiveSearch];
		if(mentionLock.location != NSNotFound) {
			if([[config getGlobalSetting:@"stickyMentions"] boolValue])
				stickyTweets = YES;
			notificationName = @"Mention!";
		}
		if([[config getGlobalSetting:@"stickyTweets"] boolValue])
			stickyTweets = YES;
		
		NSDictionary * menuData = [NSDictionary dictionaryWithObjectsAndKeys:tweetText, @"tweet", screenName, @"screenname", 
								   curID, @"id", username, @"account", nil];
		
		[GrowlApplicationBridge notifyWithTitle:title 
									description:tweetText 
							   notificationName:notificationName 
									   iconData:[profileImg TIFFRepresentation] 
									   priority:[[config getGlobalSetting:@"importantTweets"] boolValue] ? 1 : 0 
									   isSticky:stickyTweets
								   clickContext:menuData];
		NSString * menuText = [NSString stringWithFormat:@"%@: %@", screenName, tweetText];
		NSMenu * usersMenu = [((NSMenuItem*)[config getUserSetting:@"menuitem" forUser:username]) submenu];
		NSMenuItem * newItem = [usersMenu addItemWithTitle:menuText action:@selector(handleMenuAction:) keyEquivalent:@""];
		[newItem setRepresentedObject: menuData];
		NSMenu * subTweetMenu = [[NSMenu alloc] initWithTitle:@"Tweet!"];
		[newItem setSubmenu:subTweetMenu];
		newItem = [subTweetMenu addItemWithTitle:@"Open Twitter" action:@selector(handleMenuAction:) keyEquivalent:@""];
		[newItem setRepresentedObject: menuData];
		newItem = [subTweetMenu addItemWithTitle:@"Open URLs" action:@selector(handleMenuAction:) keyEquivalent:@""];
		[newItem setRepresentedObject: menuData];
		newItem = [subTweetMenu addItemWithTitle:@"Reply" action:@selector(handleMenuAction:) keyEquivalent:@""];
		[newItem setRepresentedObject: menuData];
		newItem = [subTweetMenu addItemWithTitle:@"Re-Tweet" action:@selector(handleMenuAction:) keyEquivalent:@""];
		[newItem setRepresentedObject: menuData];
	}
	NSDictionary * sessionData = [NSDictionary dictionaryWithObjectsAndKeys:maxID, @"maxid", lastTweet, @"lasttweet", nil];
	return sessionData;
}

- (void) GetNewTweets: (NSString*) username except: (NSHashTable*) alreadySeen {
	
	NSNumber * maxTweetId = [config getUserSetting:@"maxid" forUser:username];
	NSNumber * maxDMId = [config getUserSetting:@"maxdmid" forUser:username];
	NSNumber * maxMentionId = [config getUserSetting:@"maxmentionid" forUser:username];
	NSNumber * countPerUpdate = [config getGlobalSetting:@"countPerUpdate"];
	BOOL useSSL = [[config getUserSetting:@"usessl" forUser:username] boolValue];
	NSError * error = nil;
	NSString * jsonData;
	
	NSString * dmURLString = [NSString stringWithFormat:
							  @"http%@://twitter.com/direct_messages.json?count=%@&since_id=%@",
							  useSSL ? @"s" : @"", countPerUpdate, maxDMId];
	
	jsonData = [config stringWithURL:[NSURL URLWithString:dmURLString] forUser:username error:&error];
	if(jsonData == nil)
	{
		[GrowlApplicationBridge notifyWithTitle:@"Error!" 
									description: [error localizedDescription] 
							   notificationName:@"Error!" 
									   iconData:nil 
									   priority:2 
									   isSticky:[[config getGlobalSetting:@"stickyTweets"] boolValue]
								   clickContext:nil];
	}
	
	NSDictionary * dmTimeline = [self postTweetsForData:jsonData forUsername:username except: alreadySeen];
	NSNumber * newMaxDMId = [dmTimeline objectForKey:@"maxid"];
	if([newMaxDMId longLongValue] > [maxDMId longLongValue])
		[config setUserSetting:newMaxDMId forKey: @"maxdmid" forUser:username];
	
	NSString * mentionsURLString = [NSString stringWithFormat:
									@"http%@://twitter.com/statuses/mentions.json?count=%@&since_id=%@",
									useSSL ? @"s" : @"", countPerUpdate, maxMentionId];
	jsonData = [config stringWithURL:[NSURL URLWithString:mentionsURLString] forUser:username error:&error];
	if(jsonData == nil)
	{
		[GrowlApplicationBridge notifyWithTitle:@"Error!" 
									description: [error localizedDescription] 
							   notificationName:@"Error!" 
									   iconData:nil 
									   priority:2 
									   isSticky:[[config getGlobalSetting:@"stickyTweets"] boolValue]
								   clickContext:nil];
	}
	NSDictionary * mentionTimeline = [self postTweetsForData:jsonData forUsername:username except: alreadySeen];
	NSNumber * newMaxMentionId = [mentionTimeline objectForKey:@"maxid"];
	if([newMaxMentionId longLongValue] > [maxMentionId longLongValue])
		[config setUserSetting:newMaxMentionId forKey:@"maxmentionid" forUser:username];
	
	NSString * homeURLString = [NSString stringWithFormat:
							    @"http%@://api.twitter.com/1/statuses/home_timeline.json?count=%@&since_id=%@",
								useSSL ? @"s" : @"", countPerUpdate, maxTweetId];
	jsonData = [config stringWithURL:[NSURL URLWithString:homeURLString] forUser:username error:&error];
	if(jsonData == nil)
	{
		[GrowlApplicationBridge notifyWithTitle:@"Error!" 
									description: [error localizedDescription] 
							   notificationName:@"Error!" 
									   iconData:nil 
									   priority:2 
									   isSticky:[[config getGlobalSetting:@"stickyTweets"] boolValue]
								   clickContext:nil];
	}
	
	NSDictionary * homeTimeline = [self postTweetsForData:jsonData forUsername:username except: alreadySeen];
	NSString * lastTweet = [homeTimeline objectForKey:@"lasttweet"];
	if([lastTweet length] > 0)
	{
		[config setUserSetting:[homeTimeline objectForKey:@"maxid"] forKey:@"maxid" forUser:username];
		[config setUserSetting:[homeTimeline objectForKey:@"lasttweet"] forKey:@"lasttweet" forUser:username];
	}
	
	NSMenu * userMenu = [((NSMenuItem*)[config getUserSetting:@"menuitem" forUser:username]) submenu];
	while([userMenu numberOfItems] > [[config getGlobalSetting:@"countPerUpdate"] intValue])
		[userMenu removeItemAtIndex:0];
}

- (void) ProcessAccountsLoop {
	SCNetworkReachabilityFlags connectFlags = kSCNetworkReachabilityFlagsReachable;
	while(keepRunning == YES)
	{
		long long idleStop = [[config getGlobalSetting:@"stopAfterIdle"] longLongValue];
		if(idleStop != 32767) {
			if([[self getIdleTime] longLongValue] >= idleStop)
				goto sleep;
		}
		if(!SCNetworkReachabilityGetFlags(reachability, &connectFlags) || inCheck == YES || [interval intValue] == 32767)
			goto sleep;
		NSArray * usernames = [config getUsers];
		NSHashTable * alreadySeen = [NSHashTable hashTableWithOptions:0];
		for(NSInteger i = 0; i < [usernames count]; i++) {
			NSString * curUser = [usernames objectAtIndex:i];
			if([[config getUserSetting:@"oauthkey" forUser:curUser] compare: [NSString string]] == NSOrderedSame)
			{
				NSString * errorString = [NSString stringWithFormat:@"%@ is not authorized to access Twitter. Please open the preferences, double click on the user, and click 'Authorize'", curUser];
				[GrowlApplicationBridge notifyWithTitle:@"Not Authorized" description:errorString notificationName:@"Error!" iconData:nil priority:0 isSticky:NO clickContext:NO];
				continue;
			}
			[self GetNewTweets: curUser except: alreadySeen];
		}
	sleep:
		[NSThread sleepForTimeInterval:[interval floatValue]];
	}
}

- (NSDictionary *) registrationDictionaryForGrowl {
	NSArray * allNotifications = [NSArray arrayWithObjects:@"Tweet!", @"Retweet!", @"Reply!", @"Twoosh!!!", @"Mention!", @"Error!", nil];
	NSDictionary * dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
								 allNotifications, GROWL_NOTIFICATIONS_ALL,
								 allNotifications, GROWL_NOTIFICATIONS_DEFAULT,
								 nil];
	return dictionary;	
}

- (NSString *) applicationNameForGrowl {
	return @"Trowel";
}

- (void) growlNotificationWasClicked:(id)clickContext {
	NSDictionary * tweetData = (NSDictionary*)clickContext;
	[self handleTweetClick:tweetData forAction:[config getGlobalSetting:@"actionOnClick"]];
}

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
	
	reachability = SCNetworkReachabilityCreateWithName(NULL, "twitter.com");
	[tweetDialog init];
	[config initWithTwitterClient:self];
	[tweetDialog setConfig:config];
	
	NSMenuItem * theItem;
	NSMenu * theMenu = [[NSMenu alloc] initWithTitle:@"Trowel"];
	[theMenu addItem:[NSMenuItem separatorItem]];
	theItem = [theMenu addItemWithTitle:@"New Tweet" action:@selector(doNewTweet:) keyEquivalent:@""];
	[theItem setTarget: tweetDialog];
	[theItem setEnabled:YES];
	theItem = [theMenu addItemWithTitle:@"Refresh Now" action:@selector(actionCheckNow:) keyEquivalent:@""];
	[theItem setTarget:self];
	[theItem setEnabled:YES];
	theItem = [theMenu addItemWithTitle:@"Preferences" action:@selector(doPreferencesGUI:) keyEquivalent:@""];
	[theItem setTarget:config];
	[theItem setEnabled:YES];
	theItem = [theMenu addItemWithTitle:@"Stop Notifications" action:@selector(stopStartNotifications:) keyEquivalent:@""];
	[theItem setTarget: self];
	[theItem setEnabled: YES];
	theItem = [theMenu addItemWithTitle:@"Quit" action:@selector(actionQuit:) keyEquivalent:@""];
	[theItem setTarget: self];
	[theItem setEnabled: YES];
	statusMenu = theMenu;
	statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
	[statusItem setMenu:theMenu];
	[statusItem setHighlightMode:YES];
	[statusItem setToolTip:@"Trowel!"];
	[statusItem setImage:[NSImage imageNamed:@"trowelsbi.png"]];
	
	NSArray * loadedAccounts = [config getUsers];
	if([loadedAccounts count] == 0)
		[config doPreferencesGUI:self];
	for(NSInteger i = 0; i < [loadedAccounts count]; i++)
	{
		NSString * username = [loadedAccounts objectAtIndex:i];
		NSMenuItem * newUserItem = [[NSMenuItem alloc] initWithTitle:username action:NULL keyEquivalent:@""];
		NSMenu * subMenu = [[NSMenu alloc] initWithTitle:username];
		[newUserItem setSubmenu:subMenu];
		[theMenu insertItem:newUserItem atIndex:0];
		[config setUserSetting:newUserItem forKey:@"menuitem" forUser:username];
	}
	
	interval = [[config getGlobalSetting:@"interval"] copy];
	keepRunning = YES;
	[NSThread detachNewThreadSelector:@selector(ProcessAccountsLoop) toTarget:self withObject:nil];
}

- (void) stopStartNotifications: (id) sender {
	NSMenuItem * theItem = sender;
	if(keepRunning == YES) {
		keepRunning = NO;
		[theItem setTitle:@"Start Notifications"];
	}
	else {
		keepRunning = YES;
		[NSThread detachNewThreadSelector:@selector(ProcessAccountsLoop) toTarget:self withObject:nil];
		[theItem setTitle:@"Stop Notifications"];
	}

}

- (void) actionQuit: (id) sender {
	NSApplication * application = [NSApplication sharedApplication];
	keepRunning = NO;
	[config preferencesFinished:self];
	[config syncronizeAccounts];
	NSStatusBar * sb = [NSStatusBar systemStatusBar];
	[sb removeStatusItem:statusItem];
	CFRelease(reachability);
	[application terminate:self];
}

- (void) actionCheckNow: (id) sender {
	SCNetworkReachabilityFlags connectFlags = kSCNetworkReachabilityFlagsReachable;
	if(!SCNetworkReachabilityGetFlags(reachability, &connectFlags) || inCheck == YES)
		return;
	NSArray * userNames = [config getUsers];
	NSHashTable * alreadySeen = [NSHashTable hashTableWithOptions:0];
	for(NSInteger i = 0; i < [userNames count]; i++)
	{
		NSString * curUser = [userNames objectAtIndex:i];
		if([[config getUserSetting:@"oauthkey" forUser:curUser] compare: [NSString string]] == NSOrderedSame)
		{
			NSString * errorString = [NSString stringWithFormat:@"%@ is not authorized to access Twitter. Please open the preferences, double click on the user, and click 'Authorize'", curUser];
			[GrowlApplicationBridge notifyWithTitle:@"Not Authorized" description:errorString notificationName:@"Error!" iconData:nil priority:0 isSticky:NO clickContext:NO];
			continue;
		}
		[self GetNewTweets: curUser except: alreadySeen];
	}
}	

- (void) changeInterval:(NSNumber *)newInterval {
	interval = [newInterval copy];
}

- (void) handleTweetClick: (NSDictionary *) tweetData forAction: (NSString*) action {
	NSWorkspace * workspace = [NSWorkspace sharedWorkspace];
	if([action isEqual:@"Open URLs"])
	{
		NSString * tweetText = [tweetData objectForKey:@"tweet"];
		NSScanner * scanner = [NSScanner scannerWithString:tweetText];
		while(![scanner isAtEnd])
		{
			NSUInteger start = 0, end = 0;
			[scanner scanUpToString:@"http://" intoString:nil];
			start = [scanner scanLocation];
			[scanner scanUpToString:@" " intoString:nil];
			end = [scanner scanLocation];
			if(end - start > 0)
			{
				NSString * foundURL = [tweetText substringWithRange:(NSRange){start, end - start}];
				[workspace openURL:[NSURL URLWithString:foundURL]];
			}
		}
	}
	else if([action isEqual:@"Open Twitter"])
	{
		NSURL * theURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://twitter.com/%@/status/%@",
											   [tweetData objectForKey:@"screenname"],
											   [tweetData objectForKey:@"id"]]];
		[workspace openURL:theURL];
	}
	else if([action isEqual:@"Reply"])
	{
		NSNumber * tweetID = [tweetData objectForKey:@"id"];
		NSString * screenName = [tweetData objectForKey:@"screenname"];
		[tweetDialog doNewTweet:self];
		[tweetDialog setReply:tweetID toUser:screenName];
	}
	else if([action isEqual:@"Re-Tweet"])
	{
		NSNumber * tweetID = [tweetData objectForKey:@"id"];
		NSString * username = [tweetData objectForKey:@"account"];
		NSString * screenname = [tweetData objectForKey:@"screenname"];
		if([username compare: screenname] == NSOrderedSame)
			return;
		BOOL useSSL = [[config getUserSetting:@"usessl" forUser:username] boolValue];
		NSString * urlString = [NSString stringWithFormat:@"http%@://api.twitter.com/1/statuses/retweet/%@.json",
								useSSL ? @"s" : @"", tweetID];
		NSError * error;
		[config stringWithURL:[NSURL URLWithString:urlString] forUser:username error:&error forMethod: @"POST"];
	}
}

- (void) handleMenuAction: (id) sender {
	NSMenuItem * menuItem = sender;
	NSDictionary * tweetData = [menuItem representedObject];
	NSString * title = [menuItem title];
	if([title isEqual:@"Open URLs"])
		[self handleTweetClick: tweetData forAction: @"Open URLs"];
	else if([title isEqual:@"Open Twitter"])
		[self handleTweetClick: tweetData forAction: @"Open Twitter"];
	else if([title isEqual:@"Reply"])
		[self handleTweetClick: tweetData forAction: @"Reply"];
	else if([title isEqual:@"Re-Tweet"])
		[self handleTweetClick: tweetData forAction: @"Re-Tweet"];
	else
		[self handleTweetClick: tweetData forAction: [config getGlobalSetting:@"actionOnClick"]];
}

@end